var gamejs = require("gamejs");
var BinaryHeap = require("gamejs/utils/binaryheap").BinaryHeap;
var Match = require("js/match").Match;
var AntColony = require("js/antColony").AntColony;
var GameEntity = require('js/gameEntity').GameEntity;
var Explosion = require ('js/explosion').Explosion;
var utils = require("js/utils");

/**
 * Enemy: an object representing an enemy.
 * It is created on a point and then it finds a path from this to a point
 * of our planet.
 * If the enemy reaches its target, it removes some health to the player.
 *
 * @extends GameEntity

/**
 * Creates an enemy in the given point.
 *
 * @param {Match} match The Match object where this object will work.
 * @param {[number, number]} position The enemy's position, in format [x, y].
 * @param {Number} level The Enemy level [default 0].
 * @param {boolean} withAntColony A boolean value indicating if this enemy has
 * to move with AntColony(if true) or A* (if false, default).
 *
 * @throws {TypeError} If match isn't a Match object or position isn't in
 *                     the correct format.
 * @throws {RangeError} If the given position isn't inside the map
 *                     recovered from the given Match.
 * @throws {Error} If the enemy can't find a path in the map.
 * 
 * @constructor
 */
var Enemy = exports.Enemy = function(match, position, level, withAntColony) {
	if( ! (match instanceof Match)) {
		throw new TypeError("match isn't a valid Match object");
	}
	// if position isn't in the correct form the isInside method raises a
	// TypeError
	if( ! match.getMap().isInside(position)) {
		throw new RangeError("Invalid coordinates");
	}

	if ( ! utils.validIndex(level, this.constructor.LEVELS_LIST)) {
		throw new RangeError('Enemy: invalid starting level!');
	}

	//the list of levels reachable with upgrade
	//enemy can't upgrade -> 1 level
	var upgradeList = this.constructor.LEVELS_LIST.slice(level, level + 1);

	// Call the GameEntity constructor
	Enemy.superConstructor.call(
		this, match, position, upgradeList,
		false, 0, SHAPE_RECTANGULAR
	);

	// sets the path
	var targetPoint = [
		0,
		PLANET_CENTER + Math.round((Math.random() - 0.5) * PLANET_HEIGHT)
	];

	// put all the antColony data in a single object to keep Enemy cleaner
	this.antColony = {
		enabled: !! withAntColony,	// !! makes sure it's boolean
		blocked: 0, // check if the enemy is blocked
		countStop: 0, // number of problematic situations
		stops : [] //array of problematic coordinates
	};

	// definition of the path: pre-made with A*, made over time with AntColony
	if ( ! this.antColony.enabled) {
		// use A*
		this.path = findPathWithStepLength(match.getMap(), position, targetPoint, 5);
		if(this.path === null) {
			throw new Error("Can't find a path.");
		}
	}
	else {
		// use Ant Colony
		this.path = [];
		this.path[0] = [
			match.getMap().getSize()[X],
			Math.floor(Math.random() * match.getMap().getSize()[Y])
		];
	}

	this.pathIndex = 0;

	// Health informations (used to draw the lifebar)
	this.fullHealth = this.getLevelSettings().health;
	this.health = this.fullHealth;
	// Armor informations (used to draw the armorbar)
	this.fullArmor = this.getLevelSettings().armor;
	this.armor = this.fullArmor;
	// Detected flag
	this.detected = false;
	// Sets imaga Opacity
	this.alpha = this.getLevelSettings().alpha;
	// Speed informations
	this.fullSpeed = this.getLevelSettings().speed;
	this.speed = this.getLevelSettings().speed;
	this.image.setAlpha(1 - this.alpha / 100);
	this.slownessDuration = 0;
	this.damage = this.getLevelSettings().damage;
};

/**
 * The list of available levels.
 * NOTE that this isn't a list of available upgrade, so when we have to
 * pass the upgradeList to the constructor of GameEntity we pass to it just
 * one level; this make the enemy not upgradable, as it's right.
 */
Enemy.LEVELS_LIST = [
	// Level 0
	{
		image: IMAGE_ROOT + "enemies/squaredEnemy.png",
		detectedImage: IMAGE_ROOT + "enemies/squaredDetectedEnemy.png",
		health: 1, speed: 30, visibilityTime: 1500, killScore: 10, armor: 1, alpha: 40, damage:1,
		killCredit: 5
	},
	// Level 1
	{
		image: IMAGE_ROOT + "enemies/squaredEnemy.png",
		detectedImage: IMAGE_ROOT + "enemies/squaredDetectedEnemy.png",
		health: 5, speed: 40, visibilityTime: 1200, killScore: 20, armor: 2, alpha: 40,damage:2,
		killCredit: 8
	}
];

/**
 * Extending GameEntity with Enemy
 */
GameEntity.extend(Enemy);

/**
 * Subtracts health points, and kills the enemy if health reaches 0
 *
 * @param {number} hitRate The amount of health to subtract
 */
Enemy.prototype.hit = function(hitRate) {
	if (this.armor <= 0) {
		this.health -= hitRate;
	}
	else {
		oldArmor = this.armor;
		this.armor -= hitRate;
		if(this.armor < 0) //too low armor to take all the damage
			this.health -= (hitRate - oldArmor);
	}

	if (this.health <= 0) {
		// Health can't be negative
		this.health = 0;
		this.getMatch().changeScore(this.getLevelSettings().killScore);
		this.getMatch().changeCredit(this.getLevelSettings().killCredit);
		this.doKill();
		var expl = new Explosion(this.getMatch(), this.rect, 1000);
		this.getMatchMap().addExplosion(expl);
	}
};

/**
 * Perform killing action
 *
 */
Enemy.prototype.doKill = function() {
	this.kill();
};

/**
 * Slow enemy by percentage
 *
 * @param {number} hitRate The percentage of slowness
 * @param {number} duration The duration of the slowness
 */
Enemy.prototype.slow = function(hitRate, duration) {
	this.speed = this.fullSpeed * hitRate;
	this.slownessDuration = duration;
};

/**
 * Set the enemy detected for time specified in the enemy.visibilityTime
 *
 * @param {boolean} detected true to make the enemy visible, false
 *        to make it invisible
 */
Enemy.prototype.setDetected = function(detected) {
	if (detected) {
		// if detected, changes the image with the visible one
		this.detected = this.getLevelSettings().visibilityTime;
		this.image = gamejs.image.load(this.getLevelSettings().detectedImage);
	}
	else {
		this.detected = 0;
	}
};

/**
 * Return detected flag
 *
 * @return {boolean} true if enemy is detected, false otherwise
 */
Enemy.prototype.isDetected = function() {
	return this.detected > 0;
};

/**
 * Updates the position of this enemy, if needed updates the visibility timer.
 * If the enemy reaches its target, removes health points from the player.
 *
 * @param {number} msDuration The time past from the last call, in ms.
 */
Enemy.prototype.update = function(msDuration) {
	// updates the position
	var speed = this.speed;

	if (this.antColony.enabled) {
		if(this.antColony.countStop > 30){
			//antColony can't arrive to the end (30 times is the max number of tries)
			this.kill();
		}

		var next;
		var dim = this.image.getSize();

		if(this.antColony.blocked == 0) {
			// enemy isn't blocked
			next = this.getMatchMap().getAntColony().next(this.path[this.pathIndex], dim);

			// search the first coordinate repetition on its path (loop can be : -1 if there aren't repetition or the index of the first repetition )
			var loop = false;
			for (var i = 0; i <= this.pathIndex; i++) {
				if(this.path[i][0] == next[0] && this.path[i][1] == next[1]) {
					loop = i;
					break;
				}
			}

			if (loop !== false && (this.pathIndex - loop) > 5) {
				// enemy must go away from this position, 5 it's the number of the min distance from the current position and the first repetition,
				// 60 it's the number of back steps.
				var back = 60;
				this.antColony.blocked = back;
				this.antColony.countStop++;
				// return back in the path of 20 positions.
				this.pathIndex = loop - back / 3;

				var stops = this.antColony.stops;
				if (stops.length > 0 && stops[stops.length - 1][0] > next[0]) {
					// It's better to clean enemy's stall-memory because previous coordinates
					// describe another stall situation not the current
					this.antColony.stops = [];
				}

				// push new problematic position
				this.antColony.stops.push(next);
				this.antColony.blocked = back * this.antColony.stops.length;
			}
			else{
				// enemy can proceed
				this.pathIndex++;
				this.path[this.pathIndex] = next;
			}
		}
		else{
			// enemy is blocked so we can't use antColony.next(..) to get net position
			next = this.getMatchMap().getAntColony().blocked(this.path[this.pathIndex], this.antColony.stops[0], dim);
			this.pathIndex++;
			this.path[this.pathIndex] = next;
			this.antColony.blocked--;
		}
		var intIndex = this.pathIndex;

	}
	else {
		this.pathIndex += speed * (msDuration / 1000);
		var intIndex = Math.round(this.pathIndex);
	}

	if (this.slownessDuration > 0) {
		this.slownessDuration -= (msDuration / 1000);
	}
	else {
		this.speed = this.fullSpeed;
	}

	if((intIndex < this.path.length) && (this.getCenter()[0] > 0)) {
		var centerPos = this.path[intIndex];
		var dim = this.image.getSize();
		var topLeft = [
			centerPos[X] - dim[WIDTH] / 2,
			centerPos[Y] - dim[HEIGHT] / 2
		];
		this.rect = new gamejs.Rect(topLeft, dim);

		var xPosition = this.path[intIndex][X];
		if (xPosition <= 2 * speed) {
			// this enemy is going to win, 2 seconds of fading
			this.image.setAlpha(1 - (xPosition / (2 * speed)));
		}
	}
	else {
		if (this.antColony.enabled) {
			this.getMatchMap().getAntColony().done(this.path);
		}
		// this enemy win, change the player life then kill it
		this.getMatch().decreaseHealth(this.damage);
		this.kill();
	}

	// if detected, updates the timer and sets the correct image
	if(this.detected > 0) {
		this.detected -= msDuration;
		if(this.detected <= 0) {
			this.detected = 0;
			this.updateImage();
			this.image.setAlpha(1 - this.alpha / 100);
		}
	}
};

/**
 * Overwrites sprite's Draw method
 *
 * @param {gamejs.Surface} surface The Surface where draw on.
 */
Enemy.prototype.draw = function(surface) {
	// Draws the enemy
	surface.blit(this.image, this.rect.topleft);

	// Draws the life-cycle over the enemy image if detected
	if(this.isDetected() && this.health > 0) {
		gamejs.draw.arc(
			surface,
			"rgb(" + Math.round(255 * (this.fullHealth - this.health) / this.fullHealth) +
				", " + Math.round(255 * this.health / this.fullHealth) + ", 0)",
			this.rect,
			0,
			Math.round(360 * this.health / this.fullHealth),
			3
		);
		gamejs.draw.arc(
			surface,
			"rgb(0,0,255)",
			this.rect,
			0,
			Math.round(360 * this.armor / this.fullArmor),
			3
		);
	}
};

/**
 * Returns an array of coordinates of navigable points from start to target, if any.
 * To improve the performances, this method first calculates the path with a distance of
 * stepLength between consecutive points, then it calculates each small path from a point to
 * its successive, giving an array of points distanced by 1.
 *
 * @param {Map} map The Map object representing the map to cross.
 * @param {[number, number]} start Coordinates of the point from where to start, in [x, y] format.
 * @param {[number, number]} target Coordinates of the point to reach, in [x, y] format.
 * @param {number} stepLength The step length to use in the first computation of the path, to work
 * as expected it has to be reasonably smaller than the minimum number of pixel occupied by an obstacle
 * @return {Array} An array of coordinates of navigable points from start to target, if any;
 * null otherwise.
 * @throws {RangeError} If coordinates of start or target aren't inside the map.
 */
var findPathWithStepLength = function(map, start, target, stepLength) {
	var openList = new BinaryHeap(evaluateRemainingPath);
	var visited = new pointHashList();
	var closedList = new pointHashList();
	var currentStep = {};
	var theoreticallyLength = map.estimatedDistance(start, target);

	// this function evaluates the path from a point to the target,
	// it's used to keep the binary heap in the correct order
	function evaluateRemainingPath(step) {
		step.value = map.estimatedDistance(step.to, target) + step.length;
		return step.value;
	}

	// this function creates an object that provide a fast access to
	// a list of point, allowing to find a point in a small time
	function pointHashList() {
		var list = {};

		this.store = function(step) {
			list[step.to[X] + "-" + step.to[Y]] = step;
			return;
		};

		this.find = function(point) {
			return list[point[X] + "-" + point[Y]];
		};

		return this;
	}

	// this function returns true if the given point can reach the target with
	// a single step of length <= stepLength
	function reachableWithSmallerStep(point) {
		return (map.estimatedDistance(point, target) <= stepLength);
	}

	// this is an A* algorithm
	var find = false;
	currentStep = {to: start, from: null, length: 0};
	openList.push(currentStep);
	visited.store(currentStep);

	while((openList.size() > 0) && ( ! find)) {
		currentStep = openList.pop();
		// leaves a very big tolerance to avoid surrender with
		// a lot of obstacles
		if(currentStep.length > theoreticallyLength * 10) {
			// is doing too much steps
			break;
		}
		closedList.store(currentStep);

		if(reachableWithSmallerStep(currentStep.to)) {
			find = true;
			break;
		}
		var reachable = map.reachable(currentStep.to, stepLength);
		reachable.forEach(function (point) {
			if(closedList.find(point)) {
				return;
			}
			var known = visited.find(point);
			var newLength = map.estimatedDistance(currentStep.to, point) + currentStep.length;

			if(( ! known) || (known.length > newLength)) {
				if(known) {
					openList.remove(known);
				}
				var otherStep = {to: point, from: currentStep, length: newLength};
				openList.push(otherStep);
				visited.store(otherStep);
			}
		});
	}

	if(find) {
		// we have a path of points distanced by stepLength
		var path = [];
		// we don't have other big steps (of stepLength length) to do,
		// maybe we need some little steps (of 1 length)

		// start and target are inverted for convenience in the
		// adding of the points to the path in the right order
		var pathBetweenBigSteps = findPath(map, target, currentStep.to);
		if(pathBetweenBigSteps === null) {
			// algorithm can't find a path from the given points
			return null;
		}
		pathBetweenBigSteps.forEach(function (nextPoint) {
			path.unshift(nextPoint);
		});

		// avoid duplicates
		path.shift();

		while(currentStep.from !== null) {
			// start and target are inverted for convenience in the
			// adding of the points to the path in the right order
			pathBetweenBigSteps = findPath(map, currentStep.to, currentStep.from.to);
			if(pathBetweenBigSteps === null) {
				// algorithm can't find a path from the given points
				return null;
			}
			pathBetweenBigSteps.forEach(function (nextPoint) {
					path.unshift(nextPoint);
				});
			currentStep = currentStep.from;
		}

		return path;
	}
	else {
		// no path found
		return null;
	}
};

/**
 * Returns an array of coordinates of navigable points from start to target, if any.
 *
 * @param {Map} map The map to cross.
 * @param {[number, number]} start Coordinates of the point from where to start.
 * @param {[number, number]} target Coordinates of the point to reach.
 * @return {Array} An array of coordinates of navigable points from start to target, if any;
 * null otherwise.
 * @throws {RangeError} If coordinates of start or target aren't inside the map.
 */
var findPath = function(map, start, target) {
	var openList = new BinaryHeap(evaluateRemainingPath);
	var visited = new pointHashList();
	var closedList = new pointHashList();
	var currentStep = {};
	var theoreticallyLength = map.estimatedDistance(start, target);

	// this function evaluates the path from a point to the target,
	// it's used to keep the binary heap in the correct order
	function evaluateRemainingPath(step) {
		step.value = map.estimatedDistance(step.to, target) + step.length;
		return step.value;
	}

	// this function creates an object that provide a fast access to
	// a list of point, allowing to find a point in a small time
	function pointHashList() {
		var list = {};

		this.store = function(step) {
			list[step.to[X] + "-" + step.to[Y]] = step;
			return;
		};

		this.find = function(point) {
			return list[point[X] + "-" + point[Y]];
		};

		return this;
	}

	// this function returns true if two points are equal
	function pointEqual(pointA, pointB) {
		return ((pointA[X] == pointB[X]) && (pointA[Y] == pointB[Y]));
	}

	// this is an A* algorithm
	var find = false;
	currentStep = {to: start, from: null, length: 0};
	openList.push(currentStep);
	visited.store(currentStep);

	while((openList.size() > 0) && ( ! find)) {
		currentStep = openList.pop();
		// leaves a small tolerance, theoretically there aren't
		// obstacles from start to target
		if(currentStep.length > theoreticallyLength * 4) {
			// is doing too much steps
			break;
		}
		closedList.store(currentStep);
		if(pointEqual(currentStep.to, target)) {
			find = true;
			break;
		}
		var adjacent = map.adjacent(currentStep.to);
		adjacent.forEach(function (point) {
			if(closedList.find(point)) {
				return;
			}
			var known = visited.find(point);
			var newLength = map.actualDistance(currentStep.to, point) + currentStep.length;

			if(( ! known) || (known.length > newLength)) {
				if(known) {
					openList.remove(known);
				}
				var otherStep = {to: point, from: currentStep, length: newLength};
				openList.push(otherStep);
				visited.store(otherStep);
			}
		});
	}

	if(find) {
		var path = [];
		// process currentStep to have an array of positions
		while(currentStep !== null) {
			path.unshift(currentStep.to);
			currentStep = currentStep.from;
		}

		return path;
	}
	else {
		// no path found
		return null;
	}
};